/*
 * Copyright (C) 2011 Alex Kuiper <http://www.nightwhistler.net>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.nightwhistler.htmlspanner;

import android.graphics.Color;
import android.text.SpannableStringBuilder;

import com.hzdracom.epubreader.util.CSSHelp;
import com.shwread.android.qysw10000182.R;
import com.shwread.http.util.Logger;

import net.nightwhistler.htmlspanner.handlers.FontHandler;
import net.nightwhistler.htmlspanner.handlers.HeaderHandler;
import net.nightwhistler.htmlspanner.handlers.ImageHandler;
import net.nightwhistler.htmlspanner.handlers.LinkHandler;
import net.nightwhistler.htmlspanner.handlers.ListItemHandler;
import net.nightwhistler.htmlspanner.handlers.MonoSpaceHandler;
import net.nightwhistler.htmlspanner.handlers.NewLineHandler;
import net.nightwhistler.htmlspanner.handlers.PreHandler;
import net.nightwhistler.htmlspanner.handlers.StyleNodeHandler;
import net.nightwhistler.htmlspanner.handlers.StyledTextHandler;
import net.nightwhistler.htmlspanner.handlers.SubScriptHandler;
import net.nightwhistler.htmlspanner.handlers.SummaryHandler;
import net.nightwhistler.htmlspanner.handlers.SuperScriptHandler;
import net.nightwhistler.htmlspanner.handlers.attributes.AlignmentAttributeHandler;
import net.nightwhistler.htmlspanner.handlers.attributes.BorderAttributeHandler;
import net.nightwhistler.htmlspanner.handlers.attributes.StyleAttributeHandler;
import net.nightwhistler.htmlspanner.style.Style;
import net.nightwhistler.htmlspanner.style.Style.TextAlignment;
import net.nightwhistler.htmlspanner.style.StyleValue;
import net.nightwhistler.htmlspanner.tyydextension.ChapterSpan;
import net.nightwhistler.htmlspanner.tyydextension.CoverPageInfo;

import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.ContentNode;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

/**
 * HtmlSpanner provides an alternative to Html.fromHtml() from the Android
 * libraries.
 *
 * In its simplest form, just call new HtmlSpanner().fromHtml() to get a similar
 * result. The real strength is in being able to register custom NodeHandlers.
 *
 * @author work
 *
 */
public class HtmlSpanner {

	/**
	 * Temporary constant for the width of 1 horizontal em Used for calculating
	 * margins.
	 */
	public static final int HORIZONTAL_EM_WIDTH = 10;

	private Map<String, TagNodeHandler> handlers;

	private boolean stripExtraWhiteSpace = false;

	private HtmlCleaner htmlCleaner;

	private FontResolver fontResolver;

	/**
	 * Switch to determine if CSS is used
	 */
	private boolean allowStyling = true;

	/**
	 * If CSS colours are used
	 */
	private boolean useColoursFromStyle = true;

	/**
	 * Creates a new HtmlSpanner using a default HtmlCleaner instance.
	 */
	public HtmlSpanner() {
		this(createHtmlCleaner(), new SystemFontResolver());
	}

	/**
	 * Creates a new HtmlSpanner using the given HtmlCleaner instance.
	 *
	 * This allows for a custom-configured HtmlCleaner.
	 *
	 * @param cleaner
	 */
	public HtmlSpanner(HtmlCleaner cleaner, FontResolver fontResolver) {
		this.htmlCleaner = cleaner;
		this.fontResolver = fontResolver;
		this.handlers = new HashMap<String, TagNodeHandler>();

		registerBuiltInHandlers();
	}

	public FontResolver getFontResolver() {
		return this.fontResolver;
	}

	public void setFontResolver(FontResolver fontResolver) {
		this.fontResolver = fontResolver;
	}

	public FontFamily getFont(String name) {
		return this.fontResolver.getFont(name);
	}

	/**
	 * Switch to specify whether excess whitespace should be stripped from the
	 * input.
	 *
	 * @param stripExtraWhiteSpace
	 */
	public void setStripExtraWhiteSpace(boolean stripExtraWhiteSpace) {
		this.stripExtraWhiteSpace = stripExtraWhiteSpace;
	}

	/**
	 * Returns if whitespace is being stripped.
	 *
	 * @return
	 */
	public boolean isStripExtraWhiteSpace() {
		return stripExtraWhiteSpace;
	}

	/**
	 * Indicates whether the text style may be updated.
	 *
	 * If this is set to false, all CSS is ignored and the basic built-in style
	 * is used.
	 *
	 * @return
	 */
	public boolean isAllowStyling() {
		return allowStyling;
	}

	/**
	 * Switch to specify is CSS style should be used.
	 *
	 * @param value
	 */
	public void setAllowStyling(boolean value) {
		this.allowStyling = value;
	}

	/**
	 * Switch to specify if the colours from CSS should override user-specified
	 * colours.
	 *
	 * @param value
	 */
	public void setUseColoursFromStyle(boolean value) {
		this.useColoursFromStyle = value;
	}

	public boolean isUseColoursFromStyle() {
		return this.useColoursFromStyle;
	}

	/**
	 * Registers a new custom TagNodeHandler.
	 *
	 * If a TagNodeHandler was already registered for the specified tagName it
	 * will be overwritten.
	 *
	 * @param tagName
	 * @param handler
	 */
	public void registerHandler(String tagName, TagNodeHandler handler) {
		this.handlers.put(tagName, handler);
		handler.setSpanner(this);
	}

	/**
	 * Removes the handler for the given tag.
	 *
	 * @param tagName
	 *            the tag to remove handlers for.
	 */
	public void unregisterHandler(String tagName) {
		this.handlers.remove(tagName);
	}

	/**
	 * Parses the text in the given String.
	 *
	 * @param html
	 *
	 * @return a Spanned version of the text.
	 */
	public ChapterSpan fromHtml(String html, boolean needProcessCover) {
		return fromTagNode(this.htmlCleaner.clean(html), needProcessCover, null);
	}

	public ChapterSpan fromHtml(String html, boolean needProcessCover,
								CancellationCallback cancellationCallback) {
		return fromTagNode(this.htmlCleaner.clean(html), needProcessCover,
				cancellationCallback);
	}

	/**
	 * Parses the text in the given Reader.
	 *
	 * @param reader
	 * @return
	 * @throws IOException
	 */
	public ChapterSpan fromHtml(Reader reader, boolean needProcessCover)
			throws IOException {
		return fromTagNode(this.htmlCleaner.clean(reader), needProcessCover,
				null);
	}

	public ChapterSpan fromHtml(Reader reader, boolean needProcessCover,
								CancellationCallback cancellationCallback) throws IOException {
		return fromTagNode(this.htmlCleaner.clean(reader), needProcessCover,
				cancellationCallback);
	}

	/**
	 * Parses the text in the given InputStream.
	 *
	 * @param inputStream
	 * @return
	 * @throws IOException
	 */
	public ChapterSpan fromHtml(InputStream inputStream,
								boolean needProcessCover) throws IOException {
		return fromTagNode(this.htmlCleaner.clean(inputStream),
				needProcessCover, null);
	}

	public ChapterSpan fromHtml(InputStream inputStream,
								boolean needProcessCover, CancellationCallback cancellationCallback)
			throws IOException {
		return fromTagNode(this.htmlCleaner.clean(inputStream),
				needProcessCover, cancellationCallback);
	}

	/**
	 * Gets the currently registered handler for this tag.
	 *
	 * Used so it can be wrapped.
	 *
	 * @param tagName
	 * @return the registed TagNodeHandler, or null if none is registered.
	 */
	public TagNodeHandler getHandlerFor(String tagName) {
		return this.handlers.get(tagName);
	}

	/**
	 * Creates spanned text from a TagNode.
	 *
	 * @param node
	 * @return
	 */
	public ChapterSpan fromTagNode(TagNode node, boolean needProcessCover,
								   CancellationCallback cancellationCallback) {

		ChapterSpan span = new ChapterSpan();
		CoverPageInfo coverInfo = new CoverPageInfo();
		coverInfo.setHaveData(false);

		SpannableStringBuilder result = new SpannableStringBuilder();
		SpanStack stack = new SpanStack();

		applySpan(result, node, stack, needProcessCover, coverInfo,
				cancellationCallback);

		stack.applySpans(this, result);

		// for (Object sp : result.getSpans(0, result.length(),
		// MetricAffectingSpan.class)) {
		// result.removeSpan(sp);
		// }

		span.setSpannable(result);
		span.setCoverPageInfo(coverInfo);

		return span;
	}

	private static HtmlCleaner createHtmlCleaner() {
		HtmlCleaner result = new HtmlCleaner();
		CleanerProperties cleanerProperties = result.getProperties();

		cleanerProperties.setAdvancedXmlEscape(true);

		cleanerProperties.setOmitXmlDeclaration(true);
		cleanerProperties.setOmitDoctypeDeclaration(false);

		cleanerProperties.setTranslateSpecialEntities(true);
		cleanerProperties.setTransResCharsToNCR(true);
		cleanerProperties.setRecognizeUnicodeChars(true);

		cleanerProperties.setIgnoreQuestAndExclam(true);
		cleanerProperties.setUseEmptyElementTags(false);

		// cleanerProperties.setPruneTags("script,title");
		cleanerProperties.setPruneTags("script");

		return result;
	}

	private void checkForCancellation(CancellationCallback cancellationCallback) {
		if (cancellationCallback != null && cancellationCallback.isCancelled()) {
//			throw new ParsingCancelledException();
			Logger.e("Parsing Cancelled Exception");
		}
	}

	private void handleContent(SpannableStringBuilder builder, Object node,
							   SpanStack stack, CancellationCallback cancellationCallback,
							   TagNode parentNode) {

		checkForCancellation(cancellationCallback);

		ContentNode contentNode = (ContentNode) node;

		String text = TextUtil.replaceHtmlEntities(contentNode.getContent()
				.toString(), false, parentNode.getName().equalsIgnoreCase("p")
				&& parentNode.getChildIndex(contentNode) == 0);

		// String text = contentNode.getContent().toString();

		if (isStripExtraWhiteSpace()) {
			// Replace unicode non-breaking space with normal space.
			text = text.replace('\u00A0', ' ');
		}

		if (text.trim().length() > 0) {
			builder.append(text);
		}
	}

	private void applySpan(SpannableStringBuilder builder, TagNode node,
						   SpanStack stack, boolean needProcessCover, CoverPageInfo coverInfo,
						   CancellationCallback cancellationCallback) {

		checkForCancellation(cancellationCallback);

		TagNodeHandler handler = this.handlers.get(node.getName());

		if (handler == null) {
			handler = new StyledTextHandler();
			handler.setSpanner(this);
		}

		int lengthBefore = builder.length();

		// 处理天翼阅读专属封面格式数据
		processChapterCover(node, needProcessCover, coverInfo);

		// 忽略注释节点
		boolean isIgnoreChildTags = false;
		if (node.getName().equals("aside") && node.hasAttribute("epub:type")) {

			isIgnoreChildTags = true;
		}

		if (node.getName().equals("title")) {
			for (Object childNode : node.getChildren()) {
				if (childNode instanceof ContentNode) {
					coverInfo.setChapterTitle(((ContentNode) childNode)
							.getContent().toString());
				}
			}
			return;
		}

		handler.beforeChildren(node, builder, stack);

		if (!isIgnoreChildTags) {
			if (!handler.rendersContent()) {

				for (Object childNode : node.getChildren()) {

					if (childNode instanceof ContentNode) {
						handleContent(builder, childNode, stack,
								cancellationCallback, node);
					} else if (childNode instanceof TagNode) {
						applySpan(builder, (TagNode) childNode, stack,
								needProcessCover, coverInfo,
								cancellationCallback);
					}
				}
			}
		}

		int lengthAfter = builder.length();
		handler.handleTagNode(node, builder, lengthBefore, lengthAfter, stack);

	}

	private void processChapterCover(TagNode node, boolean needProcessCover,
									 CoverPageInfo coverInfo) {
		if (needProcessCover && node.getName().equals("body")
				&& node.hasAttribute("epub:type")
				&& node.getAttributeByName("epub:type").equals("cover")) {
			// 获取style
			if (node.hasAttribute("style")) {
				String styleStr = node.getAttributeByName("style");
				// 处理style
				String[] styleComponents = styleStr.split(" ");
				boolean nextBackground = false;
				for (String component : styleComponents) {
					if (component.equalsIgnoreCase("background:")) {
						nextBackground = true;
						continue;
					}
					if (nextBackground) {
						nextBackground = false;
						coverInfo.setBackgroundColor(component);
						continue;
					}
					if (component.startsWith("url(")) {
						coverInfo.setBackgroundImage(component.substring(4,
								component.length() - 1));
					}
				}
			}
			// 获取子节点
			if (node.getChildTags().length == 1) {
				// div处理
				TagNode divNode = node.getChildTags()[0];
				for (TagNode childNode : divNode.getChildTags()) {
					if (childNode.getName().equals("h1")) {
						// 大标题
						coverInfo.setTitle(childNode.getText().toString());
						coverInfo.setTitleStyle(childNode
								.getAttributeByName("class"));
					} else if (childNode.getName().equals("h2")) {
						// 副标题
						coverInfo.setSubTitle(childNode.getText().toString());
					}
				}
			}
			coverInfo.refreshStatus();

		}
	}

	private static StyledTextHandler wrap(StyledTextHandler handler,
										  boolean useCssStyle) {
		return new StyleAttributeHandler(new AlignmentAttributeHandler(handler,
				useCssStyle), useCssStyle);
	}

	private void registerBuiltInHandlers() {
		TagNodeHandler lancHandler = new StyledTextHandler(
				new Style().setColor(R.color.book_mark_bule), false);
		registerHandler("mark", lancHandler);

		TagNodeHandler italicHandler = new StyledTextHandler(
				new Style().setFontStyle(Style.FontStyle.ITALIC), false);

		registerHandler("i", italicHandler);
		registerHandler("em", italicHandler);
		registerHandler("cite", italicHandler);
		registerHandler("dfn", italicHandler);

		TagNodeHandler boldHandler = new StyledTextHandler(
				new Style().setFontWeight(Style.FontWeight.BOLD), false);

		registerHandler("b", boldHandler);
		registerHandler("strong", boldHandler);

		TagNodeHandler marginHandler = new StyledTextHandler(new Style()
				.setMarginLeft(new StyleValue(2.0f, StyleValue.Unit.EM))
				.setFontStyle(Style.FontStyle.ITALIC), false);
		TagNodeHandler blockquoteHandler = new StyledTextHandler(new Style()
				.setFontFamily(null != CSSHelp.getSomeonesaid().font_family ? CSSHelp.getSomeonesaid().font_family : getFontResolver().getDefaultFont())
				.setMarginLeft(new StyleValue(2.0f, StyleValue.Unit.EM)), true);

		registerHandler("blockquote", blockquoteHandler);
		registerHandler("ul", marginHandler);
		registerHandler("ol", marginHandler);

		TagNodeHandler monSpaceHandler = wrap(new MonoSpaceHandler(), false);

		registerHandler("tt", monSpaceHandler);
		registerHandler("code", monSpaceHandler);

		registerHandler("style", new StyleNodeHandler());

		// We wrap an alignment-handler to support
		// align attributes

//		TagNodeHandler figcaptionNodeHandler = new StyledTextHandler(
//				new Style().setFontWeight(FontWeight.BOLD).setDisplayStyle(
//						Style.DisplayStyle.BLOCK), false);
		TagNodeHandler figcaptionNodeHandler = new StyledTextHandler(
				new Style()
						.setDisplayStyle(Style.DisplayStyle.BLOCK), false);
		registerHandler("figcaption", figcaptionNodeHandler);

		TagNodeHandler figureNodeHandler = new StyledTextHandler(new Style()
				.setDisplayStyle(Style.DisplayStyle.BLOCK).setTextAlignment(
						TextAlignment.CENTER), false);
		registerHandler("figure", figureNodeHandler);

		StyledTextHandler inlineAlignment = wrap(new StyledTextHandler(), false);
		TagNodeHandler brHandler = new NewLineHandler(1, inlineAlignment);

		registerHandler("br", brHandler);

		Style paragraphStyle = new Style().setDisplayStyle(
				Style.DisplayStyle.BLOCK).setMarginBottom(
				new StyleValue(1.0f, StyleValue.Unit.EM));

		//TODO div样式
		Style divParagraphStyle = new Style()
				.setFontFamily(null != CSSHelp.getSomeonesaid().font_family ? CSSHelp.getSomeonesaid().font_family : getFontResolver().getDefaultFont())
				.setDisplayStyle(Style.DisplayStyle.BLOCK)
				.setMarginBottom(new StyleValue(1.0f, StyleValue.Unit.EM))
				.setBorderStyle(/*BorderStyle.SOLID*/ CSSHelp.getSomeonesaid().border_top_style)
				.setBorderColor(/*Color.parseColor("#BBD575")*/ Color.parseColor(CSSHelp.getSomeonesaid().border_top_color))
				.setBorderWidth(/*new StyleValue(1)*/ new StyleValue(CSSHelp.getSomeonesaid().border_bottom_width))
				.setBorderTopWidth(/*new StyleValue(5)*/ new StyleValue(CSSHelp.getSomeonesaid().border_top_width))
				.setHasBorderLeft(false)
				.setHasBorderRight(false);

		TagNodeHandler pHandler = new StyledTextHandler(paragraphStyle, false);

		TagNodeHandler divHandler = new BorderAttributeHandler(wrap(
				new StyledTextHandler(divParagraphStyle, true), true), true);

//		registerHandler("p", wrap(new StyledTextHandler(paragraphStyle, true), true));
		registerHandler("p", pHandler);
		registerHandler("div", divHandler);

		registerHandler("h1", wrap(new HeaderHandler(1.5f, 0.5f), false));
		registerHandler("h2", wrap(new HeaderHandler(1.3f, 0.6f), false));
		registerHandler("h3", wrap(new HeaderHandler(1.1f, 0.7f), false));
		registerHandler("h4", wrap(new HeaderHandler(1f, 0.8f), false));
//		registerHandler("h5", wrap(new HeaderHandler(1.1f, 0.9f), false));
//		registerHandler("h6", wrap(new HeaderHandler(1f, 1f), false));

		TagNodeHandler preHandler = new PreHandler();
		registerHandler("pre", preHandler);

		TagNodeHandler bigHandler = new StyledTextHandler(
				new Style().setFontSize(new StyleValue(1.25f,
						StyleValue.Unit.EM)), false);

		registerHandler("big", bigHandler);

		TagNodeHandler smallHandler = new StyledTextHandler(
				new Style()
						.setFontSize(new StyleValue(0.8f, StyleValue.Unit.EM)),
				false);

		registerHandler("small", smallHandler);

		TagNodeHandler subHandler = new SubScriptHandler();
		registerHandler("sub", subHandler);

		TagNodeHandler superHandler = new SuperScriptHandler();
		registerHandler("sup", superHandler);

		TagNodeHandler centerHandler = new StyledTextHandler(
				new Style().setTextAlignment(Style.TextAlignment.CENTER), false);
		registerHandler("center", centerHandler);

		registerHandler("li", new ListItemHandler());

		registerHandler("a", new LinkHandler());
		registerHandler("img", new ImageHandler());

		registerHandler("font", new FontHandler());

//		registerHandler("summary", new StyledTextHandler(new Style().setTextAlignment(TextAlignment.RIGHT), false));
		registerHandler("summary", new SummaryHandler(CSSHelp.getSummary().margin_left, CSSHelp.getSummary().margin_right));

	}

	public static interface CancellationCallback {
		boolean isCancelled();
	}

}